#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <termios.h>
#include <string.h>

#include <pcd_api.h>

static int pcd_fd = 0;
static struct termios oldtio, newtio;

LIST_HEAD(pcd_join_list);

static void __pcd_dump_reports(u_int8 *raw_data, u_int8 length)
{
    int i;

    fprintf(stderr, "[Dump reports]\n");
    for (i = 0; i < length; i++) {
        fprintf(stderr, "%x ", raw_data[i]);
        if (i > 0 && (i % 8) == 0)
            fprintf(stderr, "\n");
    }
    fprintf(stderr, "\n\n");
}

static int __pcd_get_reports(u_int8 *raw_data, u_int8 length, struct pcd_report_data *reports)
{
    u_int8 *ptr = raw_data;
    u_int8 value_type;
    u_int8 value_length;
    int warn = 0;

    /* Reset reports */
    reports->voltage = 0;
    reports->current = 0;
    reports->instant_power = 0;
    reports->accum_power = 0;

    //__pcd_dump_reports(raw_data, length);

    while (ptr < raw_data + length) {
        value_length = *ptr & 0xf;
        value_type = (*ptr >> 4) & 0xf;
        ++ptr;

        switch (value_type) {
            case REPORT_TYPE_VOLT:
                if (value_length == 2) {
                    /* Fixed 2-byte value */
                    reports->voltage = *ptr | *(ptr + 1) << 8;
                    ptr += 2;
                }
                else {
                    printf("Warning: [%d] Bad value length: %d\n", value_type, value_length);
                    ++warn;
                }
                break;
            case REPORT_TYPE_CURR:
                if (value_length == 2) {
                    reports->current = *ptr | *(ptr + 1) << 8;
                    ptr += 2;
                }
                else {
                    printf("Warning: [%d] Bad value length: %d\n", value_type, value_length);
                    ++warn;
                }
                break;
            case REPORT_TYPE_INSTPWR:
                if (value_length == 2) {
                    reports->instant_power = *ptr | *(ptr + 1) << 8;
                    ptr += 2;
                }
                else {
                    printf("Warning: [%d] Bad value length: %d\n", value_type, value_length);
                    ++warn;
                }
                break;
            case REPORT_TYPE_ACCPWR:
                if (value_length == 2) { /* FIXME: Len = 2? */
                    reports->accum_power = *ptr | *(ptr + 1) << 8;
                    ptr += 2;
                }
                else {
                    printf("Warning: [%d] Bad value length: %d\n", value_type, value_length);
                    ++warn;
                }
                break;
            default:
                printf("Warning: Unsupported value type: %d\n", value_type);
                ++warn;
                break;
        }
    }

    return warn;
}

static void __pcd_dump_packet(struct pcd_raw_pkt *packet)
{
    int i;

    fprintf(stderr, "[START] %c%c%c%c\n", packet->start[0], packet->start[1], packet->start[2], packet->start[3]);
    fprintf(stderr, "[ADDRESS] %d\n", packet->address);
    fprintf(stderr, "[COMMAND] 0x%x\n", packet->command);
    fprintf(stderr, "[LENGTH] %d\n", packet->length);
    fprintf(stderr, "[CHECKSUM] 0x%x\n", packet->checksum);
    fprintf(stderr, "[DATA]\n");

    for (i = 0; i < packet->length; i++) {
        fprintf(stderr, "%x ", packet->data[i]);
        if (i > 0 && (i % 8) == 0)
            fprintf(stderr, "\n");
    }
    fprintf(stderr, "\n\n");
}

static u_int8 __pcd_checksum(struct pcd_raw_pkt *raw)
{
    u_int8 sum;
    int i;

    sum = raw->command + raw->length;

    for (i = 0; i < raw->length; i++)
        sum += raw->data[i];

    return sum & 0xff;
}

static int __pcd_read_raw_packet(struct pcd_raw_pkt *raw)
{
    int len;
    int count;
    u_int8 checksum;

    if (!raw)
        return ERROR_INVALID_POINTER;

    while (1) {
        /* Search for packet START byte */
        if (raw->start[0] != '_') {
            count = read(pcd_fd, &raw->start[0], 1);
            if (raw->start[0] != '_')
                continue;
        }
        count = read(pcd_fd, &raw->start[1], 1);
        if (raw->start[1] == '_') {
            raw->start[0] = '_';
            continue;
        }
        if (raw->start[1] != 'P') {
            raw->start[0] = 0;
            continue;
        }
        count = read(pcd_fd, &raw->start[2], 1);
        if (raw->start[2] == '_') {
            raw->start[0] = '_';
            continue;
        }
        if (raw->start[2] != 'S') {
            raw->start[0] = 0;
            continue;
        }
        count = read(pcd_fd, &raw->start[3], 1);
        if (raw->start[3] == '_') {
            raw->start[0] = '_';
            continue;
        }
        if (raw->start[3] != 'D') {
            raw->start[0] = 0;
            continue;
        }

        /* Read address */
        if (read(pcd_fd, (char *)&raw->address, 1) == 0)
            return ERROR_BAD_ADDRESS;
        if (read(pcd_fd, (char *)&raw->address + 1, 1) == 0)
            return ERROR_BAD_ADDRESS;

        /* Read command */
        if (read(pcd_fd, (char *)&raw->command, 1) == 0)
            return ERROR_BAD_COMMAND;

        /* Read length */
        if (read(pcd_fd, (char *)&raw->length, 1) == 0)
            return ERROR_BAD_LENGTH;

        /* Get the number of byte available in the input buffer */
        ioctl(pcd_fd, FIONREAD, &count);

        if (count < raw->length)
            printf("Warning: available data in buffer is less then raw packet length\n");

        /* Read data */
        len = read(pcd_fd, &raw->data, raw->length);

        //printf("Number of data read in byte: %d\n", len);

        /* Read checksum */
        if (read(pcd_fd, (char *)&raw->checksum, 1) == 0)
            return ERROR_BAD_CHECKSUM;

        /* Checksum */
        checksum = __pcd_checksum(raw);
        //printf("Checksum = 0x%x\n", checksum);

        if (checksum != raw->checksum) {
            fprintf(stderr, "Bad checksum!\n");
            return ERROR_BAD_CHECKSUM;
        }

        return 0;
    }
}

static int __pcd_write_raw_packet(struct pcd_raw_pkt *raw)
{
    int offset = 0;

    while (offset < 8 + raw->length)
        offset += write(pcd_fd, &((char *)raw)[offset], 8 + raw->length - offset);

    raw->checksum = __pcd_checksum(raw);
    offset = write(pcd_fd, &raw->checksum, 1);

    return 0;
}

void pcd_open(const char *pcd_dev)
{
    if (!pcd_dev) {
        fprintf(stderr, "PCD device not specified\n");
        exit(-1);
    }

    pcd_fd = open(pcd_dev, O_RDWR | O_NOCTTY);
    if (pcd_fd < 0) {
        perror(pcd_dev);
        exit(-1);
    }

    /* save current serial port settings */
    tcgetattr(pcd_fd,&oldtio);
    /* clear struct for new port settings */
    bzero(&newtio, sizeof(newtio));

    /* Setting (baudrate, data bits, flow control, stop bit) to (115200,8,n,1) */
    newtio.c_cflag = B115200 | CS8 | CLOCAL | CREAD;
    /* Choosing raw input */
    newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    /* Choosing raw output */
    newtio.c_oflag &= ~OPOST;

    /* Set inter-character timer to 5ms */
    newtio.c_cc[VTIME] = 5;
    /* Set minimum chars received */
    newtio.c_cc[VMIN] = 255;

    /* now clean the modem line and activate the settings for the port */
    tcflush(pcd_fd, TCIFLUSH);
    tcsetattr(pcd_fd, TCSANOW, &newtio);

    printf("Port initialized\n");
}

void pcd_close(void)
{
    tcsetattr(pcd_fd, TCSANOW, &oldtio);
    close(pcd_fd);
    pcd_fd = 0;
}

struct pcd_struct *pcd_read_packet(void)
{
    struct pcd_raw_pkt raw;
    struct pcd_struct *pcd_info;
    int result;

    result = __pcd_read_raw_packet(&raw);

    if (result < 0) {
        fprintf(stderr, "Error while reading packets, code = -%d\n", result);
        return NULL;
    }

    pcd_info = malloc(sizeof(struct pcd_struct));
    pcd_info->id = raw.address;
    pcd_info->command = raw.command;

    switch (pcd_info->command) {
        case RCMD_JOIN:
            snprintf(pcd_info->mac_addr, 24, "%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x",
                     raw.data[0], raw.data[1], raw.data[2], raw.data[3],
                     raw.data[4], raw.data[5], raw.data[6], raw.data[7]);
            break;
        case RCMD_REPORT:
            result = __pcd_get_reports(raw.data, raw.length, &pcd_info->reports);
            if (result) {
                /* TODO: We got warnings, should we do something? */
            }
            break;
        case RCMD_MAC:
            snprintf(pcd_info->mac_addr, 24, "%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x",
                     raw.data[0], raw.data[1], raw.data[2], raw.data[3],
                     raw.data[4], raw.data[5], raw.data[6], raw.data[7]);
            break;
        case RCMD_STATUS:
            pcd_info->time_left = raw.data[0] | (raw.data[1] << 8);
            pcd_info->status = raw.data[2];
            break;
        case RCMD_EVENT:
            pcd_info->event = raw.data[0];
            break;
        /*case RCMD_ALARM:
            pcd_info->alarm = raw.data[0];
            break;*/
        case RCMD_AUTO_REPORT:
            result = __pcd_get_reports(raw.data, raw.length, &pcd_info->reports);
            if (result) {
                /* TODO: We got warnings, should we do something? */
            }
            break;
        case RCMD_COORD_ACK:
            pcd_info->ack = raw.data[0];
    }

    //__pcd_dump_packet(&raw);

    return pcd_info;
}

void pcd_report_request(u_int16 id)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_REPORT_REQ;
    raw.length = 0;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_set_autoreport_timer(u_int16 id, u_int16 time)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_SET_AUTO_REPORT_TIMER;
    raw.length = 2;
    raw.data[0] = time & 0xff;
    raw.data[1] = (time >> 8) & 0xff;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_report_type_select(u_int16 id, u_int16 selection)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_REPORT_SELECT;
    raw.length = 2;
    raw.data[0] = selection & 0xff;
    raw.data[1] = (selection >> 8) & 0xff;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_set_turnon_timer(u_int16 id, u_int16 time)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_TIMER_ON;
    raw.length = 2;
    raw.data[0] = time & 0xff;
    raw.data[1] = (time >> 8) & 0xff;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_set_turnoff_timer(u_int16 id, u_int16 time)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_TIMER_OFF;
    raw.length = 2;
    raw.data[0] = time & 0xff;
    raw.data[1] = (time >> 8) & 0xff;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_mac_request(u_int16 id)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_MAC_REQ;
    raw.length = 0;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_status_request(u_int16 id)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_STATUS_REQ;
    raw.length = 0;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

#if 0
void pcd_set_max_volt(u_int16 id, u_int16 volt)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_SET_MAX_VOLT;
    raw.length = 2;
    raw.data[0] = volt & 0xff;
    raw.data[1] = (volt >> 8) & 0xff;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_set_min_volt(u_int16 id, u_int16 volt)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_SET_MIN_VOLT;
    raw.length = 2;
    raw.data[0] = volt & 0xff;
    raw.data[1] = (volt >> 8) & 0xff;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_set_max_curr(u_int16 id, u_int16 curr)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_SET_MAX_CURR;
    raw.length = 2;
    raw.data[0] = curr & 0xff;
    raw.data[1] = (curr >> 8) & 0xff;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}

void pcd_set_min_curr(u_int16 id, u_int16 curr)
{
    struct pcd_raw_pkt raw;

    raw.start[0] = '_';
    raw.start[1] = 'P';
    raw.start[2] = 'S';
    raw.start[3] = 'D';
    raw.address = id;
    raw.command = TCMD_SET_MIN_CURR;
    raw.length = 2;
    raw.data[0] = curr & 0xff;
    raw.data[1] = (curr >> 8) & 0xff;
    raw.checksum = __pcd_checksum(&raw);

    __pcd_write_raw_packet(&raw);
}
#endif

int pcd_list_insert(struct pcd_struct *pcd)
{
    struct pcd_struct *pcd_info;

    pcd_info = malloc(sizeof(struct pcd_struct));
    memcpy(pcd_info, pcd, sizeof(struct pcd_struct));
    INIT_LIST_HEAD(&pcd_info->list_node);
    list_add_tail(&pcd_info->list_node, &pcd_join_list);

    return 0;
}

struct pcd_struct *pcd_list_search_by_id(u_int16 id)
{
    struct pcd_struct *pcd_info;
    struct list_head *list_node;

    list_for_each(list_node, &pcd_join_list) {
        if (!list_node) {
            fprintf(stderr, "WARNING: Broken PCD list!\n");
            break;
        }

        pcd_info = list_entry(list_node, struct pcd_struct, list_node);
        if (!pcd_info) {
            fprintf(stderr, "WARNING: Broken PCD entry!\n");
            break;
        }

        if (pcd_info->id == id)
            return pcd_info;
    }
    return NULL;
}

struct pcd_struct *pcd_list_search_by_mac(char *mac)
{
    struct pcd_struct *pcd_info;
    struct list_head *list_node;

    list_for_each(list_node, &pcd_join_list) {
        if (!list_node) {
            fprintf(stderr, "WARNING: Broken PCD list!\n");
            break;
        }

        pcd_info = list_entry(list_node, struct pcd_struct, list_node);
        if (!pcd_info) {
            fprintf(stderr, "WARNING: Broken PCD entry!\n");
            break;
        }

        if (strncmp(pcd_info->mac_addr, mac, 23) == 0)
            return pcd_info;
    }
    return NULL;
}
